package com.jeebey.mybatis.interceptor;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.CachingExecutor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.SimpleExecutor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.transaction.Transaction;

import com.jeebey.mybatis.dialect.Dialect;
import com.jeebey.mybatis.dialect.IDialect;
import com.jeebey.mybatis.dialect.impl.DB2Dialect;
import com.jeebey.mybatis.dialect.impl.H2Dialect;
import com.jeebey.mybatis.dialect.impl.HSQLDialect;
import com.jeebey.mybatis.dialect.impl.InformixDialect;
import com.jeebey.mybatis.dialect.impl.MariadbDialect;
import com.jeebey.mybatis.dialect.impl.MysqlDialect;
import com.jeebey.mybatis.dialect.impl.OracleDialect;
import com.jeebey.mybatis.dialect.impl.PostgreSqlDialect;
import com.jeebey.mybatis.dialect.impl.SqlServer2012Dialect;
import com.jeebey.mybatis.dialect.impl.SqlServerDialect;
import com.jeebey.mybatis.dialect.impl.SqliteDialect;
import com.jeebey.mybatis.query.Bounds;
import com.jeebey.mybatis.query.Pagination;
import com.jeebey.mybatis.query.Paging;
import com.jeebey.mybatis.utils.MappedStatmentUtil;
import com.jeebey.mybatis.utils.SqlExecutorUtil;
import com.jeebey.mybatis.utils.SqlGenerateUtil;

/**分页插件
 * @since 1.0
 * @author <a href="mailto:84961426@qq.com">JuST4iT</a>
 * @version $Id: PaginationInterceptor.java Feb 18, 2016 6:05:39 PM $
 */

@Intercepts({ @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
		RowBounds.class, ResultHandler.class }) })
public class PaginationInterceptor implements Interceptor {

	@SuppressWarnings({ "rawtypes", "unchecked" })
	public Object intercept(Invocation invocation) throws Throwable {
		Object targetObject = invocation.getTarget();
		MetaObject executorMetaObject = MetaObject.forObject(targetObject, new DefaultObjectFactory(),
				new DefaultObjectWrapperFactory(), new DefaultReflectorFactory());
		while (executorMetaObject.hasGetter("h") || executorMetaObject.hasGetter("target")) {
			Object _hTargetObj = executorMetaObject.hasGetter("h") ? executorMetaObject.getValue("h") : null;
			Object _tTargetObj = executorMetaObject.hasGetter("target") ? executorMetaObject.getValue("target") : null;
			targetObject = _hTargetObj != null ? _hTargetObj : _tTargetObj;
			executorMetaObject = MetaObject.forObject(targetObject, new DefaultObjectFactory(),
					new DefaultObjectWrapperFactory(), new DefaultReflectorFactory());
		}

		if (targetObject instanceof CachingExecutor|| targetObject instanceof SimpleExecutor) {
			final MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
			final Object parameterObject = invocation.getArgs()[1];
			
			String sourceSql = mappedStatement.getBoundSql(parameterObject).getSql();
			RowBounds rowBounds = (RowBounds) invocation.getArgs()[2];
			if (SqlCommandType.SELECT == mappedStatement.getSqlCommandType()) {
				Bounds bounds=null;
				Paging<?> paging = null;
				if ((parameterObject instanceof Paging)) {
					paging = (Paging<?>) parameterObject;
					rowBounds = new RowBounds(paging.getFrom(), paging.getSize());
				} else if(parameterObject instanceof Bounds){
					bounds = (Bounds) parameterObject;
					rowBounds = new RowBounds(bounds.getFrom(), bounds.getSize());
				} else if (parameterObject instanceof MapperMethod.ParamMap) {
					MapperMethod.ParamMap<?> parameterMap = (MapperMethod.ParamMap<?>) parameterObject;
					if (parameterMap.containsKey(Paging.PAGING)) {
						paging = (Paging<?>) parameterMap.get(Paging.PAGING);
						rowBounds = new RowBounds(paging.getFrom(), paging.getSize());
					} else if(parameterMap.containsKey(Bounds.BOUNDS)) {
						bounds = (Bounds) parameterMap.get(Bounds.BOUNDS);
						rowBounds = new RowBounds(bounds.getFrom(), bounds.getSize());
					} else {
						Iterator<?> itor = parameterMap.values().iterator();
						while (itor.hasNext()) {
							Object obj = itor.next();
							if (obj instanceof Paging) {
								paging = (Paging<?>) obj;
								rowBounds = new RowBounds(paging.getFrom(), paging.getSize());
								break;
							}
							
							if (obj instanceof Bounds) {
								bounds = (Bounds) obj;
								rowBounds = new RowBounds(bounds.getFrom(), bounds.getSize());
								break;
							}
						}
					}
				}
				if (bounds !=null){
					List<ParameterMapping> parameterMappings = mappedStatement.getBoundSql(parameterObject)
							.getParameterMappings();
					
					Map<String,Object> parameterMap = (Map<String,Object>) parameterObject;
					for(ParameterMapping parameterMapping:parameterMappings){
					   	Object value=null;
				        String propertyName = parameterMapping.getProperty();
				        if (mappedStatement.getBoundSql(parameterObject).hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
				          value = mappedStatement.getBoundSql(parameterObject).getAdditionalParameter(propertyName);
				        } else if (parameterObject == null) {
				          value = null;
				        } else if (mappedStatement.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass())) {
				          value = parameterObject;
				        } else {
				          MetaObject metaObject = mappedStatement.getConfiguration().newMetaObject(parameterObject);
				          value = metaObject.getValue(propertyName);
				        }
				        parameterMap.put(parameterMapping.getProperty(), (Object)value);
					}
					
					invocation.getArgs()[0] = MappedStatmentUtil.createPaginationMappedStatement(mappedStatement,
							sourceSql, parameterObject, parameterMappings, rowBounds, dialect);
					return invocation.proceed();
				}
				if (paging != null) {
					Executor executor = (Executor) invocation.getTarget();
					Transaction transaction = executor.getTransaction();
					List<ParameterMapping> parameterMappings = mappedStatement.getBoundSql(parameterObject).getParameterMappings();
					
					Map<String,Object> parameterMap = (Map<String,Object>) parameterObject;
					for(ParameterMapping parameterMapping:parameterMappings){
					   	Object value=null;
				        String propertyName = parameterMapping.getProperty();
				        if (mappedStatement.getBoundSql(parameterObject).hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
				          value = mappedStatement.getBoundSql(parameterObject).getAdditionalParameter(propertyName);
				        } else if (parameterObject == null) {
				          value = null;
				        } else if (mappedStatement.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass())) {
				          value = parameterObject;
				        } else {
				          MetaObject metaObject = mappedStatement.getConfiguration().newMetaObject(parameterObject);
				          value = metaObject.getValue(propertyName);
				        }
				        parameterMap.put(parameterMapping.getProperty(), (Object)value);
					}
					
					long iTotal = SqlExecutorUtil.getCount(transaction.getConnection(), mappedStatement,
							parameterObject, SqlGenerateUtil.createSqlSelectCount(sourceSql), parameterMappings);
					invocation.getArgs()[0] = MappedStatmentUtil.createPaginationMappedStatement(mappedStatement,
							sourceSql, parameterObject, parameterMappings, rowBounds, dialect);
					List<?> items=iTotal > 0 ? ((List) invocation.proceed()) : Collections.EMPTY_LIST;
					return new Pagination(iTotal, paging.getPage(), paging.getSize(), items);
				}
			}
		}
		return invocation.proceed();
	}

	public Object plugin(Object object) {
		return Plugin.wrap(object, this);
	}

	public void setProperties(Properties properties) {
		String _dialectType = properties.getProperty("dialect");
		if (StringUtils.isEmpty(_dialectType)) {
			dialect = new MysqlDialect();
			return;
		}
		Dialect _dialect = Dialect.of(_dialectType);
		switch (_dialect) {
		case mysql:
			dialect = new MysqlDialect();break;
		case mariadb:
			dialect = new MariadbDialect();break;
		case sqlite:
			dialect = new SqliteDialect();break;
		case oracle:
			dialect = new OracleDialect();break;
		case hsql:
			dialect = new HSQLDialect();break;
		case sqlserver:
			dialect = new SqlServerDialect();break;
		case sqlserver2012:
			dialect = new SqlServer2012Dialect();break;
		case db2:
			dialect = new DB2Dialect();break;
		case postgresql:
			dialect = new PostgreSqlDialect();break;
		case informix:
			dialect = new InformixDialect();break;
		case h2:
			dialect = new H2Dialect();break;
		default:
			dialect = new MysqlDialect();
		}
	}

	private IDialect dialect = new MysqlDialect();
}
